--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 8d5bef20972771dafba705d9ab939902715543db
Parents : 2fb42d2
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-14T11:02:04-06:00
Update NetworkVisualiser with abort controller and improved data fetching
- Increased page size for path table and announces fetching to 1000 for better performance.
- Implemented an AbortController to manage request cancellations, improving responsiveness during data loading.
- Refactored data fetching methods to support concurrent requests while respecting the abort signal.
- Updated loading status messages to reflect current progress during data retrieval.
Changes
1 files changed, 109 insertions(+), 44 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
index eabb001b..d3c90a26 100644
--- a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
+++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
@@ -300,8 +300,9 @@ export default {
edges: new DataSet(),
iconCache: {},
- pageSize: 100,
+ pageSize: 1000,
searchQuery: "",
+ abortController: new AbortController(),
};
},
computed: {
@@ -345,6 +346,9 @@ export default {
},
},
beforeUnmount() {
+ if (this.abortController) {
+ this.abortController.abort();
+ }
if (this._toggleOrbitHandler) {
GlobalEmitter.off("toggle-orbit", this._toggleOrbitHandler);
}
@@ -384,72 +388,121 @@ export default {
methods: {
async getInterfaceStats() {
try {
- const response = await window.axios.get(`/api/v1/interface-stats`);
+ const response = await window.axios.get(`/api/v1/interface-stats`, {
+ signal: this.abortController.signal,
+ });
this.interfaces = response.data.interface_stats?.interfaces ?? [];
} catch (e) {
+ if (window.axios.isCancel(e)) return;
console.error("Failed to fetch interface stats", e);
}
},
async getPathTableBatch() {
this.pathTable = [];
- let offset = 0;
- let totalCount = 1; // dummy initial value
-
- while (offset < totalCount) {
- this.loadingStatus = `Loading Paths (${offset} / ${totalCount === 1 ? "..." : totalCount})`;
- try {
- const response = await window.axios.get(`/api/v1/path-table`, {
- params: { limit: this.pageSize, offset: offset },
- });
- this.pathTable.push(...response.data.path_table);
- totalCount = response.data.total_count;
- offset += this.pageSize;
- } catch (e) {
- console.error("Failed to fetch path table batch", e);
- break;
+ try {
+ this.loadingStatus = "Loading Paths...";
+ const firstResp = await window.axios.get(`/api/v1/path-table`, {
+ params: { limit: this.pageSize, offset: 0 },
+ signal: this.abortController.signal,
+ });
+ this.pathTable.push(...firstResp.data.path_table);
+ const totalCount = firstResp.data.total_count;
+
+ if (totalCount > this.pageSize) {
+ const remainingOffsets = [];
+ for (let offset = this.pageSize; offset < totalCount; offset += this.pageSize) {
+ remainingOffsets.push(offset);
+ }
+
+ // Fetch remaining batches in parallel with limited concurrency to not overwhelm backend
+ const concurrency = 3;
+ for (let i = 0; i < remainingOffsets.length; i += concurrency) {
+ if (this.abortController.signal.aborted) return;
+ const chunk = remainingOffsets.slice(i, i + concurrency);
+ const promises = chunk.map((offset) =>
+ window.axios.get(`/api/v1/path-table`, {
+ params: { limit: this.pageSize, offset: offset },
+ signal: this.abortController.signal,
+ })
+ );
+ const responses = await Promise.all(promises);
+ for (const r of responses) {
+ this.pathTable.push(...r.data.path_table);
+ }
+ this.loadingStatus = `Loading Paths (${this.pathTable.length} / ${totalCount})`;
+ }
}
+ } catch (e) {
+ if (window.axios.isCancel(e)) return;
+ console.error("Failed to fetch path table batch", e);
}
},
async getAnnouncesBatch() {
this.announces = {};
- let offset = 0;
- let totalCount = 1;
-
- while (offset < totalCount) {
- this.loadingStatus = `Loading Announces (${offset} / ${totalCount === 1 ? "..." : totalCount})`;
- try {
- const response = await window.axios.get(`/api/v1/announces`, {
- params: { limit: this.pageSize, offset: offset },
- });
+ try {
+ this.loadingStatus = "Loading Announces...";
+ const firstResp = await window.axios.get(`/api/v1/announces`, {
+ params: { limit: this.pageSize, offset: 0 },
+ signal: this.abortController.signal,
+ });
+
+ for (const announce of firstResp.data.announces) {
+ this.announces[announce.destination_hash] = announce;
+ }
+ const totalCount = firstResp.data.total_count;
- for (const announce of response.data.announces) {
- this.announces[announce.destination_hash] = announce;
+ if (totalCount > this.pageSize) {
+ const remainingOffsets = [];
+ for (let offset = this.pageSize; offset < totalCount; offset += this.pageSize) {
+ remainingOffsets.push(offset);
}
- totalCount = response.data.total_count;
- offset += this.pageSize;
- } catch (e) {
- console.error("Failed to fetch announces batch", e);
- break;
+ const concurrency = 3;
+ for (let i = 0; i < remainingOffsets.length; i += concurrency) {
+ if (this.abortController.signal.aborted) return;
+ const chunk = remainingOffsets.slice(i, i + concurrency);
+ const promises = chunk.map((offset) =>
+ window.axios.get(`/api/v1/announces`, {
+ params: { limit: this.pageSize, offset: offset },
+ signal: this.abortController.signal,
+ })
+ );
+ const responses = await Promise.all(promises);
+ for (const r of responses) {
+ for (const announce of r.data.announces) {
+ this.announces[announce.destination_hash] = announce;
+ }
+ }
+ this.loadingStatus = `Loading Announces (${Object.keys(this.announces).length} / ${totalCount})`;
+ }
}
+ } catch (e) {
+ if (window.axios.isCancel(e)) return;
+ console.error("Failed to fetch announces batch", e);
}
},
async getConfig() {
try {
- const response = await window.axios.get("/api/v1/config");
+ const response = await window.axios.get("/api/v1/config", {
+ signal: this.abortController.signal,
+ });
this.config = response.data.config;
} catch (e) {
+ if (window.axios.isCancel(e)) return;
console.error("Failed to fetch config", e);
}
},
async getConversations() {
try {
- const response = await window.axios.get(`/api/v1/lxmf/conversations`);
+ const response = await window.axios.get(`/api/v1/lxmf/conversations`, {
+ signal: this.abortController.signal,
+ });
this.conversations = {};
for (const conversation of response.data.conversations) {
this.conversations[conversation.destination_hash] = conversation;
}
} catch (e) {
+ if (window.axios.isCancel(e)) return;
console.error("Failed to fetch conversations", e);
}
},
@@ -516,6 +569,11 @@ export default {
const svgBlob = new Blob([iconSvg], { type: "image/svg+xml" });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
+ if (this.abortController.signal.aborted) {
+ URL.revokeObjectURL(url);
+ resolve(null);
+ return;
+ }
// Draw a subtle shadow for the icon itself
ctx.shadowColor = "rgba(0,0,0,0.2)";
ctx.shadowBlur = 4;
@@ -536,6 +594,11 @@ export default {
resolve(dataUrl);
};
img.onerror = () => {
+ if (this.abortController.signal.aborted) {
+ URL.revokeObjectURL(url);
+ resolve(null);
+ return;
+ }
URL.revokeObjectURL(url);
const dataUrl = canvas.toDataURL();
this.iconCache[cacheKey] = dataUrl;
@@ -916,9 +979,11 @@ export default {
this.totalBatches = 0;
await Promise.all([this.getConfig(), this.getInterfaceStats(), this.getConversations()]);
+ if (this.abortController.signal.aborted) return;
this.loadingStatus = "Fetching network data...";
await Promise.all([this.getPathTableBatch(), this.getAnnouncesBatch()]);
+ if (this.abortController.signal.aborted) return;
await this.processVisualization();
},
@@ -1017,12 +1082,13 @@ export default {
const aspectsToShow = ["lxmf.delivery", "nomadnetwork.node"];
- // Process in chunks of 25 for smooth visual updates
- const chunkSize = 25;
+ // Process in larger chunks for speed, but keep UI responsive
+ const chunkSize = 250;
this.totalBatches = Math.ceil(this.pathTable.length / chunkSize);
this.currentBatch = 0;
for (let i = 0; i < this.pathTable.length; i += chunkSize) {
+ if (this.abortController.signal.aborted) return;
this.currentBatch++;
const chunk = this.pathTable.slice(i, i + chunkSize);
const batchNodes = [];
@@ -1085,6 +1151,7 @@ export default {
conversation.lxmf_user_icon.background_colour,
64
);
+ if (this.abortController.signal.aborted) return;
node.size = 30;
} else {
node.shape = "circularImage";
@@ -1155,13 +1222,11 @@ export default {
// Allow UI to breathe and show progress
this.loadingStatus = `Processing Batch ${this.currentBatch} / ${this.totalBatches}...`;
- // Faster batching: only delay if there's many nodes, and use a shorter delay
- if (this.pathTable.length > 100) {
- await new Promise((r) => setTimeout(r, 10));
- } else {
- // Small networks update instantly
- await this.$nextTick();
- }
+
+ // Use nextTick for responsiveness
+ await this.$nextTick();
+
+ if (this.abortController.signal.aborted) return;
}
// Cleanup: remove nodes/edges that are no longer in the network
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────